package redisstore

import (
	"context"
	"errors"
	"fmt"
	"gitee.com/zackeus/go-zero/core/stores/redis"
	"gitee.com/zackeus/goutil/arrutil"
	"gitee.com/zackeus/goutil/strutil"
	"strings"
	"time"
)

const (
	// 过期时长系数
	expireThreshold  = 1.2
	defaultScanCount = 50
)

type (
	// RedisStore session 存储(基于 redis)
	RedisStore struct {
		client       *redis.Redis
		indexPrefix  string // 索引键前缀(一般使用用户ID作为索引) session数据键前缀 ${namespace}:index:${indexId}:${sessionId}
		dataPrefix   string // session数据键前缀 ${namespace}:data:${sessionId}
		expirePrefix string // 过期键前缀 ${namespace}:expire:${sessionId}
	}
)

// New 返回一个新的 RedisStore 实例
func New(prefix string, client *redis.Redis) (*RedisStore, error) {
	/* 前缀是否以 : 结尾 */
	if !strings.HasSuffix(prefix, ":") {
		prefix = prefix + ":"
	}

	return &RedisStore{
		client:       client,
		indexPrefix:  prefix + "index:",
		dataPrefix:   prefix + "data:",
		expirePrefix: prefix + "expire:",
	}, nil
}

// 数据键过期(需大于过期键 防止过期键过期后无法获取token数据)
func (r *RedisStore) dataExpire(expiry time.Time) (time.Time, error) {
	now := time.Now().UTC()
	/* 计算UTC时差 */
	duration := expiry.UTC().Sub(now)
	if duration < 0 {
		return time.Time{}, errors.New("expiry time is in the past")
	}
	return now.Add(time.Duration(float64(duration) * expireThreshold)), nil
}

// 扫描keys
func (r *RedisStore) scanKeys(ctx context.Context, match string) ([]string, error) {
	var cursor uint64
	var keys []string
	for {
		var val []string
		var err error
		val, cursor, err = r.client.ScanCtx(ctx, cursor, match, defaultScanCount)
		if err != nil {
			return nil, err
		}

		keys = append(keys, val...)
		// 没有更多key了
		if cursor == 0 {
			break
		}
	}

	/* 列表去重 */
	return arrutil.Unique(keys), nil
}

// DataKey 数据键
func (r *RedisStore) DataKey(token string) string {
	return fmt.Sprintf("%s%v", r.dataPrefix, token)
}

// ExpireKey 过期键
func (r *RedisStore) ExpireKey(token string) string {
	return fmt.Sprintf("%s%v", r.expirePrefix, token)
}

// IndexKey 索引键
func (r *RedisStore) IndexKey(indexId string, token string) string {
	return fmt.Sprintf("%s%v:%v", r.indexPrefix, indexId, token)
}

func (r *RedisStore) Create(ctx context.Context, indexId string, token string, b []byte, expiry time.Time) (err error) {
	dataExpiry, err := r.dataExpire(expiry)
	if err != nil {
		return err
	}

	err = r.client.TxPipelinedCtx(ctx, func(pipeliner redis.Pipeliner) (err error) {
		/* 数据 */
		pipeliner.Set(ctx, r.DataKey(token), string(b), 0)
		pipeliner.PExpireAt(ctx, r.DataKey(token), dataExpiry)
		/* 过期 */
		pipeliner.Set(ctx, r.ExpireKey(token), expiry.String(), 0)
		pipeliner.PExpireAt(ctx, r.ExpireKey(token), expiry)
		/* 索引 */
		pipeliner.Set(ctx, r.IndexKey(indexId, token), expiry.String(), 0)
		pipeliner.PExpireAt(ctx, r.IndexKey(indexId, token), expiry)
		return nil
	})
	return err
}

// Commit 更新
func (r *RedisStore) Commit(ctx context.Context, indexId string, token string, b []byte, expiry time.Time) (err error) {
	dataExpiry, err := r.dataExpire(expiry)
	if err != nil {
		return err
	}

	/* 这里 只更新数据键数据 其他只更新过期时长 防止高并发过期键删除又被set */
	err = r.client.TxPipelinedCtx(ctx, func(pipeliner redis.Pipeliner) (err error) {
		/* 数据 */
		pipeliner.Set(ctx, r.DataKey(token), string(b), 0)
		pipeliner.PExpireAt(ctx, r.DataKey(token), dataExpiry)
		/* 过期 */
		pipeliner.PExpireAt(ctx, r.ExpireKey(token), expiry)
		/* 索引 */
		pipeliner.PExpireAt(ctx, r.IndexKey(indexId, token), expiry)
		return nil
	})
	return err
}

func (r *RedisStore) Get(ctx context.Context, token string) ([]byte, bool, error) {
	/* 先检查过期键是否存在 */
	exist, err := r.client.ExistsCtx(ctx, r.ExpireKey(token))
	if err != nil {
		return nil, false, err
	}
	if !exist {
		return nil, false, nil
	}

	data, err := r.client.GetCtx(ctx, r.DataKey(token))
	if err != nil {
		return nil, false, err
	}
	/* 数据为空 不存在 */
	if strutil.IsBlank(data) {
		return nil, false, nil
	}

	return []byte(data), true, nil
}

func (r *RedisStore) Find(ctx context.Context, indexId string) (map[string][]byte, error) {
	/* 构建匹配字符 */
	match := fmt.Sprintf("%s%v:", r.indexPrefix, indexId)

	keys, err := r.scanKeys(ctx, match+"*")
	if err != nil {
		return nil, err
	}
	sessions := make(map[string][]byte)

	for _, key := range keys {
		token := key[len(match):]
		// TODO 考虑使用 conn.MGet().Result() 多个key查询
		data, exists, err := r.Get(ctx, token)
		if exists {
			sessions[token] = data
		}

		if err != nil {
			return nil, err
		}
	}

	return sessions, nil
}

// Delete 清楚指定indexId里的token
func (r *RedisStore) Delete(ctx context.Context, indexId string, token string) (err error) {
	_, err = r.client.DelCtx(ctx,
		r.DataKey(token),
		r.ExpireKey(token),
		r.IndexKey(indexId, token),
	)
	return err
}

// Clear 清楚指定indexId下的所有token
func (r *RedisStore) Clear(ctx context.Context, indexId string) (err error) {
	/* 构建匹配字符 */
	match := fmt.Sprintf("%s%v:", r.indexPrefix, indexId)

	keys, err := r.scanKeys(ctx, match+"*")
	if err != nil {
		return err
	}
	if len(keys) == 0 {
		return nil
	}

	tokens := make([]string, 0, len(keys))
	for _, key := range keys {
		tokens = append(tokens, key[len(match):])
	}

	/* 待删除键集合 */
	doDelKeys := make([]string, 0, len(keys))
	for _, token := range tokens {
		doDelKeys = append(doDelKeys, r.DataKey(token), r.ExpireKey(token), r.IndexKey(indexId, token))
	}
	_, err = r.client.DelCtx(ctx, doDelKeys...)
	return nil
}

func (r *RedisStore) All(ctx context.Context) (map[string][]byte, error) {
	keys, err := r.scanKeys(ctx, r.dataPrefix+"*")
	if err != nil {
		return nil, err
	}

	sessions := make(map[string][]byte)
	for _, key := range keys {
		token := key[len(r.dataPrefix):]
		data, exists, err := r.Get(ctx, token)
		if exists {
			sessions[token] = data
		}

		if err != nil {
			return nil, err
		}
	}
	return sessions, nil
}
